iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 8
0
Modern Web

JS Design Pattern 系列 第 8

JS Design Pattern Day08-組合模式 Composite

  • 分享至 

  • xImage
  •  

嗨嗨大家好,今天第八天,真好奇大家準備一篇文章都要多久,原本以為只要一兩個小時,結果遠比我想像的還花時間TT

今天寫的是 組合模式

組合模式將物件組合成樹狀結構,回顧上一個章節(Day7命令模式)中最後一個範例,我們做了一個巨集命令,巨集命令裡包含了幾個子命令,這樣就成了一個簡單的組合模式。組合模式帶來相當大的便利性,通過組合物件(marcoCommand)的execute方法,程式會呼叫基本物件(closeDoorCommand等物件)的execute方法,所以使用上只需要呼叫一次即可,也可以讓使用者忽略物件上的不同,只要每個物件都有個execute方法無論是組合物件還是基本物件都可以加入組和。

我們來做一個更強大的巨集命令。做一個button,內涵的巨集指令會做完所有的子命令:

var $btn1 = (function() {
	return $('<button>').text('MarcoCommandButton').appendTo('body');
})();

var MarcoCommand = function() {
	return {
		commandList: [],
		add: function(cmd) {
			this.commandList.push(cmd);
		},
		execute: function() {
			this.commandList.forEach(function(cmd) {
				cmd.execute();
			});
		}
	};
};

var openAcCommad = {
	execute: function() {
		console.log('openAC');
	}
};
var openTvCommad = {
	execute: function() {
		console.log('openTV');
	}
};
var openPcCommad = {
	execute: function() {
		console.log('openPC');
	}
};

var marcoCommand1 = MarcoCommand();
marcoCommand1.add(openAcCommad);
marcoCommand1.add(openTvCommad);
marcoCommand1.add(openPcCommad);

var openFbCommad = {
	execute: function() {
		console.log('openFB');
	}
};
var openSpotifyCommad = {
	execute: function() {
		console.log('openSpotify');
	}
};
var openChromeCommad = {
	execute: function() {
		console.log('openChrome');
	}
};

var marcoCommand2 = MarcoCommand();
marcoCommand2.add(openFbCommad);
marcoCommand2.add(openSpotifyCommad);
marcoCommand2.add(openChromeCommad);


var marcoCommandAll = MarcoCommand();
marcoCommandAll.add(marcoCommand1);
marcoCommandAll.add(marcoCommand2);


$btn1.click(function() {
	marcoCommandAll.execute();
});

在整個組合完成時,所有物件看起來就像樹狀結構一樣。最終執行的方式很簡單,只需要執行最上層物件(marcoCommandAll)的execute函數就可以了,而這棵樹可以任意的增加組合模組,增加這些物件的程式設計師還可以忽略物件細節,讓整個過程相當方便。基本上組合模式最大優點在於可以一致的對待組合物件(marcoCommand1)和基本物件(openChromeCommad),只需要知道物件都有一個execute方法可以用來執行也可以被加入樹裡就好就好,但這樣的透明性也是有可能帶來缺點的,比如我們往基本物件加入節點。
解決方法就是在基本物件上面也增加add方法,並且回傳錯誤:

var openChromeCommad = {
	execute: function() {
		console.log('openChrome');
	},
	add: function() {
		throw new Error('不能加入子節點');
	}
};

電腦的檔案與資料夾結構其實很符合組合模式,每個檔案可以任意地加入資料夾,資料夾也可以任意的加入其他資料夾,當執行像是病毒掃描之類的動作時會掃描每一個資料夾裡的每一個檔案,我們來實作一下:

首先,我們先做folder物件,裏面有名稱與一個陣列用來記錄內含的檔案有哪些

var Folder = function(name) {
	this.name = name;
	this.files = [];
};

實作加入檔案功能

Folder.prototype.add = function(file) {
	this.files.push(file);
};

實作掃描功能

Folder.prototype.scan = function() {
	console.log('now scanning 資料夾:' + this.name);
	this.files.forEach(function(file) {
		file.scan();
	});
};

再來,實作file物件,基本上跟folder差不多,只不過file是基本物件所以add功能要回應錯誤訊息

var File = function(name) {
	this.name = name;
};
File.prototype.add = function() {
	throw new Error('檔案無法增加物件');
};
File.prototype.scan = function() {
	console.log('now scanning 檔案:' + this.name);
};

做好了,我們來使用一下,實際產生file並加入資料夾

var folder = new Folder('電影');
var folder1 = new Folder('Action');
var folder2 = new Folder('Thriller');
var folder3 = new Folder('S');

var file1 = new File('MI6');
var file2 = new File('Happy Death Day');
var file3 = new File('sss');

folder1.add(file1);
folder2.add(file2);
folder3.add(file3);

folder.add(folder1);
folder.add(folder2);
folder.add(folder3);

最後我們假設要掃描所有檔案,只要指定最上層資料夾掃瞄,就可以看到所有資料夾跟檔案都被掃瞄過

folder.scan();

我們最後加上刪除功能,刪除檔案實際上是從這個檔案所在的上層資料夾中刪除資料,所以我們要替每個物件加上parent選項:

var Folder = function(name) {
	this.name = name;
	this.parent = null;
	this.files = [];
};

增加節點的時後要加上parent的資訊,而掃描功能是不變的

Folder.prototype.add = function(file) {
	file.parent = this;
	this.files.push(file);
};

Folder.prototype.scan = function() {
	console.log('now scanning 資料夾:' + this.name);
	this.files.forEach(function(file) {
		file.scan();
	});
};

實作加入刪除的功能

Folder.prototype.remove = function() {
	var self = this;
	if (!this.parent) {
		return;
	}
	this.parent.files.forEach(function(file, i, files) {
		if (file === self) {
			files.splice(i, 1);
		}
	});
};

file物件一樣加入parent,大體跟folder上一樣

var File = function(name) {
	this.name = name;
	this.parent = null;
};
File.prototype.add = function() {
	throw new Error('檔案無法增加物件');
};
File.prototype.scan = function() {
	console.log('now scanning 檔案:' + this.name);
};
File.prototype.remove = function() {
	var self = this;
	if (!this.parent) {
		return;
	}
	this.parent.files.forEach(function(file, i, files) {
		if (file === self) {
			files.splice(i, 1);
		}
	});
};

最後我們再來實際產生file並加入資料夾

var folder = new Folder('電影');
var folder1 = new Folder('Action');
var folder2 = new Folder('Thriller');
var folder3 = new Folder('S');

var file1 = new File('MI6');
var file2 = new File('Happy Death Day');
var file3 = new File('sss');

folder1.add(file1);
folder2.add(file2);
folder3.add(file3);

folder.add(folder1);
folder.add(folder2);
folder.add(folder3);

任意刪除一資料夾,再來掃描就可以看出結果囉

folder1.remove();
folder.scan();

ok以上就是組合模式啦,最後再補充一下,組合模式適用於以下場景:
1.表示物件的部分-整體結構,像是樹狀物件,可以方便增刪節點,並且符合開放-封閉原則
2.所有物件可以統一,使用起來忽略組合物件和基本物件之間的區別,所有物件各自做自己正確的事


上一篇
JS Design Pattern Day07-命令模式 Command
下一篇
JS Design Pattern Day09-範本方法模式 TemplateMethod
系列文
JS Design Pattern 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
快樂
iT邦新手 5 級 ‧ 2018-10-24 13:51:38

準備時間要看內容欸
像大大這麼有深度的內容

準備好幾個小時
甚至可以說是花了從母胎至今探索的經驗+學術彙總
那時間就算不太出來了呢

期待大大能持續寫下去

0
fx777
iT邦新手 5 級 ‧ 2018-10-24 18:08:26

要看內容深度與熟悉度耶~而且事後修來修去的零碎時間加起來也很可觀 (真正可怕的地方)~
像我前兩天文章前後就大概花了 7 小時 (還要收集資料、規劃等等),
然後普通文章其後都至少 1.5 ~ 3 小時起跳 QQ

我要留言

立即登入留言